#!/usr/bin/env node
// 使用xhdfs的时候，需要安装krb5，安装krb5需要安装依赖
const os = require('os');
const fsextra = require('fs-extra');
const path = require('path');
const moment = require('moment');
const hdfs = require('hdfs247');
const command = require('./js/command');
const utils = require('./js/utils');
const _ = require('lodash');
const program = require('commander');
const shell = require('shelljs');
const configFile = path.join(os.homedir(), '.hdfs_config');
const CACHE = {};
let _host = 'localhost';
let _port = 50070;
let _user = 'fang';
let _pwd = '/';

function createHDFS(commands, options) {
    const fs = new hdfs({ protocol: 'http', hostname: _host, port: _port });
    // 自动完成
    async function completer(line, callback) {
        const words = line.trim().split(/\s/);
        let list = [];
        if (words.length <= 1) {
            const cmds = ['help', 'cls', 'pwd', 'whoami', 'ls', 'll', 'mkdir', 'cd', 'mv', 'cp', 'rm', 'touch', 'echo', 'cat', 'upload', 'download', 'xcat', 'lg'];
            const _linePartial = line.trim();
            list = cmds.filter((o) => o.startsWith(_linePartial));
            return callback(null, [list, _linePartial]);
        }
        const lastWord = _.last(words);
        const items = lastWord.split('/');
        const linePartial = _.last(items);
        const prepath = items.slice(0, -1).join('/');
        const firstWord = words[0];
        if (_.includes(['mv', 'cp', 'rm', 'cat', 'upload', 'download', 'xcat', 'lg'], firstWord)) {
            list = await getAllFiles(fs, prepath);
        } else if (_.includes(['ls', 'll', 'mkdir', 'cd', 'touch', 'echo'], firstWord)) {
            list = await getDirs(fs, prepath);
        }
        const found = list.filter((o) => o.startsWith(linePartial));
        return callback(null, [found.length ? found : list, linePartial]);
    }
    CACHE._options = { ...options, fs, completer };
    command(commands, CACHE._options);
}
function getSize(length) {
    let unit = 'B';
    if (length > 1024) {
        length = (length / 1024).toFixed(2);
        unit = 'K';
        if (length > 1024) {
            length = (length / 1024).toFixed(2);
            unit = 'M';
            if (length > 1024) {
                length = (length / 1024).toFixed(2);
                unit = 'G';
            }
        }
    }
    return length + unit;
}
function getAllFiles(fs, path) {
    if (CACHE.lastAllPath && CACHE.lastAllPath === path) {
        return CACHE.allFiles;
    }
    return new Promise(resolve => {
        fs.liststatus(getOption(path), (error, response, body) => {
            if (error) {
                resolve([]);
            } else {
                const list = _.filter(getData(body, 'FileStatuses.FileStatus', []), o => o.type === 'FILE' || o.type === 'DIRECTORY').map(o => o.pathSuffix);
                CACHE.lastAllPath = path;
                CACHE.allFiles = list;
                resolve(list);
            }
        });
    });
}
function getDirs(fs, path) {
    if (CACHE.lastDirPath && CACHE.lastDirPath === path) {
        return CACHE.dirs;
    }
    return new Promise(resolve => {
        fs.liststatus(getOption(path), (error, response, body) => {
            if (error) {
                resolve([]);
            } else {
                const list = _.filter(getData(body, 'FileStatuses.FileStatus', []), o => o.type === 'DIRECTORY').map(o => o.pathSuffix);
                CACHE.lastDirPath = path;
                CACHE.dirs = list;
                resolve(list);
            }
        });
    });
}
function getFiles(fs, path) {
    if (CACHE.lastFilePath && CACHE.lastFilePath === path) {
        return CACHE.files;
    }
    return new Promise(resolve => {
        fs.liststatus(getOption(path), (error, response, body) => {
            if (error) {
                resolve([]);
            } else {
                const list = _.filter(getData(body, 'FileStatuses.FileStatus', []), o => o.type === 'FILE').map(o => o.pathSuffix);
                CACHE.lastFilePath = path;
                CACHE.files = list;
                resolve(list);
            }
        });
    });
}
function getData(body, keys, defaultValue) {
    return _.get(JSON.parse(body || '{}'), keys) || defaultValue;
}
function getPath(line) {
    line = /^\//.test(line) ? line : path.join(_pwd, line);
    return line.replace(/\\/g, '/');
}
function getOption(line, param = {}) {
    return {
        'user.name': _user,
        path: getPath(line),
        ...param,
    };
}
function pwd(line = '', { fs }) {
    _pwd = path.join(_pwd, line);
    this.print(`${_pwd}`, 'gray');
    this.prompt();
}
function whoami(line, { fs }) {
    this.print(`${_user}`, 'gray');
    this.prompt();
}
function cd(line = '', { fs }) {
    if (!line) {
        _pwd = '/';
    } else {
        _pwd = path.join(_pwd, line);
    }
    this.setPrompt(`hdfs@${_user}:${_pwd} $\n`);
    this.prompt();
}
function ls(line = '', { fs }) {
    fs.liststatus(getOption(line), (error, response, body) => {
        if (error) {
            this.error(error);
        } else {
            const list = _.sortBy(getData(body, 'FileStatuses.FileStatus', []), 'type');
            this.printArray(list.map(o => ({ msg: o.pathSuffix + '  ', color: o.type === 'FILE' ? 'green' : 'blue' })));
            this.prompt();
        }
    });
}
function ll(line = '', { fs }) {
    fs.liststatus(getOption(line), (error, response, body) => {
        if (error) {
            this.error(error);
        } else {
            const list = _.sortBy(getData(body, 'FileStatuses.FileStatus', []), 'type');
            let texts = [];
            texts[0] = { msg: _.padEnd('name', 30), color: 'black' };
            texts[1] = { msg: _.padEnd('group', 15), color: 'black' };
            texts[2] = { msg: _.padEnd('owner', 10), color: 'black' };
            texts[3] = { msg: _.padEnd('permission', 10), color: 'black' };
            texts[4] = { msg: _.padEnd('size', 10), color: 'black' };
            texts[5] = { msg: _.padEnd('accessTime', 25), color: 'black' };
            texts[6] = { msg: 'modificationTime', color: 'black' };
            this.printArray(texts);
            for (const file of list) {
                texts = [];
                texts[0] = { msg: _.padEnd(file.pathSuffix, 30), color: file.type === 'FILE' ? 'green' : 'blue' };
                texts[1] = { msg: _.padEnd(file.group, 15), color: 'gray' };
                texts[2] = { msg: _.padEnd(file.owner, 10), color: 'gray' };
                texts[3] = { msg: _.padEnd(file.permission, 10), color: 'gray' };
                texts[4] = { msg: _.padEnd(getSize(file.length), 10), color: 'gray' };
                texts[5] = { msg: _.padEnd(moment(file.accessTime).format('YYYY-MM-DD HH:mm:ss'), 25), color: 'gray' };
                texts[6] = { msg: moment(file.modificationTime).format('YYYY-MM-DD HH:mm:ss'), color: 'gray' };
                this.printArray(texts);
            }
            this.prompt();
        }
    });
}
function mkdir(line = '', { fs }) {
    if (!line) {
        return this.error('use mdir xx[ yy]');
    }
    const list = line.split(/\s+/);
    for (const item of list) {
        fs.mkdirs(getOption(item), (error, response, body) => {
            const except = getData(body, 'RemoteException.message');
            if (error) {
                this.error(error);
            } else if (except) {
                this.error(except);
            } else {
                this.prompt();
            }
        });
    }
}
function mv(line = '', { fs }) {
    if (!line) {
        return this.error('no file');
    }
    const list = line.split(/\s+/);
    fs.rename(getOption(list[0], { destination: getPath(list[1]) }), (error, response, body) => {
        const except = getData(body, 'RemoteException.message');
        if (error) {
            this.error(error);
        } else if (except) {
            this.error(except);
        } else {
            this.prompt();
        }
    });
}
function rm(line = '', { fs }) {
    if (!line) {
        return this.error('no file');
    }
    fs.delete(getOption(line, { recursive: true }), (error, response, body) => {
        const except = getData(body, 'RemoteException.message');
        if (error) {
            this.error(error);
        } else if (except) {
            this.error(except);
        } else {
            this.prompt();
        }
    });
}
function cp(line = '', { fs }) {
    if (!line) {
        return this.error('no file');
    }
    const list = line.split(/\s+/);
    shell.exec(`hadoop fs -cp "${getPath(list[0])}" "${getPath(list[1])}"`, { silent:true }, (code, stdout, stderr) => {
        stderr && this.error(stderr);
        this.prompt();
    });
}
function touch(line = '', { fs }) {
    if (!line) {
        return this.error('no file');
    }
    fs.create(getOption(line, { overwrite: false }), (error, response, body) => {
        const except = getData(body, 'RemoteException.message');
        if (error) {
            this.error(error);
        } else if (except) {
            this.error(except);
        } else {
            this.prompt();
        }
    });
}
function echo(line = '', { fs }) {
    const matches = line.match(/(.*[^>])(>{1,2})\s*([.\w]+)$/);
    if (matches) {
        const text = matches[1];
        const op = matches[2];
        const file = matches[3];
        if (op === '>') {
            fsextra.writeFileSync('.__temp_file', text, 'utf-8');
            fs.upload(getOption(file, { overwrite: true, localpath: '.__temp_file' }), (error, response, body) => {
                fsextra.unlink('.__temp_file');
                const except = getData(body, 'RemoteException.message');
                if (error) {
                    this.error(error);
                } else if (except) {
                    this.error(except);
                } else {
                    this.prompt();
                }
            });
        } else {
            fs.getfilestatus(getOption(file), (error, response, body) => {
                if (getData(body, 'FileStatus.type') === 'FILE') {
                    fsextra.writeFileSync('.__temp_file', '\n' + text, 'utf-8');
                    fs.append(getOption(file, { localpath: '.__temp_file' }), (error, response, body) => {
                        fsextra.unlink('.__temp_file');
                        const except = getData(body, 'RemoteException.message');
                        if (error) {
                            this.error(error);
                        } else if (except) {
                            this.error(except);
                        } else {
                            this.prompt();
                        }
                    });
                } else {
                    fsextra.writeFileSync('.__temp_file', text, 'utf-8');
                    fs.upload(getOption(file, { overwrite: false, localpath: '.__temp_file' }), (error, response, body) => {
                        fsextra.unlink('.__temp_file');
                        const except = getData(body, 'RemoteException.message');
                        if (error) {
                            this.error(error);
                        } else if (except) {
                            this.error(except);
                        } else {
                            this.prompt();
                        }
                    });
                }
            });
        }
    } else {
        this.print(`${line}`, 'gray');
        this.prompt();
    }
}
function cat(line = '', { fs }) {
    if (!line) {
        return this.error('no file');
    }
    fs.open(getOption(line), (error, response, body) => {
        if (error) {
            this.error(error);
        } else {
            this.print(body, 'gray');
            this.prompt();
        }
    });
}
function upload(line = '', { fs }) {
    if (!line) {
        return this.error('no file');
    }
    let pc_path, hdfs_path;
    if (/.*\s+.*/.test(line)) {
        const list = line.split(/\s+/);
        pc_path = list[0];
        hdfs_path = list[1];
    } else {
        pc_path = line;
        const list = line.split('/');
        hdfs_path = _.last(list);
    }
    fs.getfilestatus(getOption(hdfs_path), (error, response, body) => {
        if (getData(body, 'FileStatus.type') === 'DIRECTORY') {
            fs.upload(getOption(path.join(hdfs_path, _.last(pc_path.split('/'))), { overwrite: false, localpath: pc_path }), (error, response, body) => {
                const except = getData(body, 'RemoteException.message');
                if (error) {
                    this.error(error);
                } else if (except) {
                    this.error(except);
                } else {
                    this.prompt();
                }
            });
        } else if (getData(body, 'FileStatus.type') === 'FILE') {
            return this.error('file exists');
        } else {
            fs.upload(getOption(hdfs_path, { overwrite: false, localpath: pc_path }), (error, response, body) => {
                const except = getData(body, 'RemoteException.message');
                if (error) {
                    this.error(error);
                } else if (except) {
                    this.error(except);
                } else {
                    this.prompt();
                }
            });
        }
    });
}
function download(line = '', { fs }) {
    if (!line) {
        return this.error('no file');
    }
    let pc_path, hdfs_path;
    if (/.*\s+.*/.test(line)) {
        const list = line.split(/\s+/);
        hdfs_path = list[0];
        pc_path = list[1];
    } else {
        hdfs_path = line;
        const list = line.split('/');
        pc_path = path.join(os.homedir(), _.last(hdfs_path));
    }
    fs.getfilestatus(getOption(hdfs_path), (error, response, body) => {
        if (getData(body, 'FileStatus.type') === 'DIRECTORY') {
            return this.error('can not download directory');
        } else if (getData(body, 'FileStatus.type') === 'FILE') {
            fs.download(getOption(hdfs_path), (error, response, body) => {
                if (error) {
                    this.error(error);
                } else {
                    if (fsextra.existsSync(pc_path)) {
                        if (fsextra.statSync(pc_path).isDirectory()) {
                            const dist = path.join(pc_path, _.last(hdfs_path.split('/')));
                            if (fsextra.existsSync(dist)) {
                                shell.rm(body);
                                this.error(`${dist} exists, cannot download`);
                            } else {
                                shell.mv(body, dist);
                                this.print(dist, 'gray');
                            }
                        } else {
                            shell.rm(body);
                            this.error(`${pc_path} exists, cannot download`);
                        }
                    } else {
                        fsextra.ensureFile(pc_path);
                        shell.mv(body, pc_path);
                        this.print(pc_path, 'gray')
                    }
                    this.prompt();
                }
            });
        } else {
            return this.error('file not exists');
        }
    });

}
function xcat(line = '', { fs }) {
    if (!line) {
        return this.error('no file');
    }
    fs.open(getOption(line), (error, response, body) => {
        if (error) {
            this.error(error);
        } else {
            utils.copy(body);
            this.prompt();
        }
    });
}
function lg(line = '', { fs }) {
    if (!line) {
        return this.error('no file');
    }
    fs.getfilestatus(getOption(line), (error, response, body) => {
        if (getData(body, 'FileStatus.type') === 'DIRECTORY') {
            return this.error('must log to file');
        } else if (getData(body, 'FileStatus.type') === 'FILE') {
            return this.error('file exists');
        } else {
            utils.paste((text) => {
                fsextra.writeFileSync('.__temp_file', text, 'utf-8');
                fs.upload(getOption(line, { overwrite: false, localpath: '.__temp_file' }), (error, response, body) => {
                    fsextra.unlink('.__temp_file');
                    const except = getData(body, 'RemoteException.message');
                    if (error) {
                        this.error(error);
                    } else if (except) {
                        this.error(except);
                    } else {
                        this.prompt();
                    }
                });
            });
        }
    });
}
function showHelp() {
    this.print('commands:', 'blue');
    this.print('    h: show help');
    this.print('    help: show full help');
    this.print('    <q|exit>: exit');
    this.print('    whoami pwd cd ls ll mkdir mv cp rm touch echo cat upload download xcat lg');
    this.print('    upload pc_path[ hdfs_path] (xx/yy->./yy)');
    this.print('    download hdfs_path[ pc_path] (xx/yy->~/yy)');
    this.print('');
    this.prompt();
}
const COMMANDS = {
    'h': showHelp,
    'help': showHelp,
    'q|exit': function () { this.close() },
    'cls': function () { this.clear() },
    'pwd': pwd,
    'whoami': whoami,
    'ls': ls,
    'll': ll,
    'mkdir': mkdir,
    'cd': cd,
    'mv': mv,
    'cp': cp,
    'rm': rm,
    'touch': touch,
    'echo': echo,
    'cat': cat,
    'upload': upload,
    'download': download,
    'xcat': xcat,
    'lg': lg,
};


program
    .version('1.0.0')
    .option('-t, --host [host]', 'set host', '')
    .option('-p, --port [port]', 'set port', '')
    .option('-u, --user [user]', 'set user', '')
    .option('-c, --config', 'show config', '')
    .parse(process.argv);

let options = {};
try {
    options = fsextra.readJSONSync(configFile);
} catch (e) { }
_host = program.host || options.host || _host;
_port = program.port || options.port || _port;
_user = program.user || options.user || _user;
if (options.host !== _host || options.port !== _port || options.user !== _user) {
    fsextra.writeJSONSync(configFile, { host: _host, port: _port, user: _user });
}
if (program.config) {
    console.log({ host: _host, port: _port, user: _user });
    process.exit(0);
}

createHDFS(COMMANDS, { title: 'hdfs', prompt: `hdfs@${_user}:${_pwd} $\n`, history: ['h', 'help', 'cls', 'pwd', 'whoami', 'ls', 'll', 'mkdir', 'cd', 'mv', 'cp', 'rm', 'touch', 'echo', 'cat', 'upload', 'download', 'xcat', 'lg'] });
